package org.budo.dubbo.protocol.http;

import java.util.HashMap;
import java.util.Map;

import org.budo.dubbo.protocol.http.api.RegistryApi;
import org.budo.dubbo.protocol.http.cipher.Cipher;
import org.budo.dubbo.protocol.http.cipher.CipherUtil;
import org.budo.dubbo.protocol.http.exception.consumer.HttpRequestFailRuntimeException;
import org.budo.dubbo.protocol.http.invocation.BudoDubboRpcInvocation;
import org.budo.dubbo.protocol.http.sign.SignCheck;
import org.budo.dubbo.protocol.http.sign.SignEntity;
import org.budo.dubbo.protocol.http.util.BudoReflectionUtil;
import org.budo.dubbo.protocol.http.util.BudoSerializationUtil;
import org.budo.dubbo.serialize.util.DubboSerializeUtil;
import org.budo.support.http.client.Request;
import org.budo.support.http.client.Response;
import org.budo.support.lang.util.ReflectUtil;

import com.alibaba.dubbo.common.Constants;
import com.alibaba.dubbo.common.URL;
import com.alibaba.dubbo.common.extension.ExtensionLoader;
import com.alibaba.dubbo.common.serialize.Serialization;
import com.alibaba.dubbo.config.ApplicationConfig;
import com.alibaba.dubbo.config.ReferenceConfig;
import com.alibaba.dubbo.rpc.Invocation;
import com.alibaba.dubbo.rpc.Result;
import com.alibaba.dubbo.rpc.RpcContext;
import com.alibaba.dubbo.rpc.RpcException;
import com.alibaba.dubbo.rpc.RpcInvocation;
import com.alibaba.dubbo.rpc.service.GenericService;

import lombok.extern.slf4j.Slf4j;

/**
 * @author lmw
 * @see org.budo.dubbo.protocol.http.dispatcher.servlet.BudoDubboHttpProtocolDispatcherServlet
 * @see org.budo.dubbo.protocol.http.BudoDubboHttpProtocol
 * @see com.alibaba.dubbo.rpc.filter.ContextFilter
 */
@Slf4j
public class BudoDubboHttpInvoker<T> extends AbstractBudoDubboHttpInvoker<T> {
    private static final ExtensionLoader<Serialization> SERIALIZATION_EXTENSION_LOADER = ExtensionLoader.getExtensionLoader(Serialization.class);

    public BudoDubboHttpInvoker(Class<T> type, URL url) {
        this.setInterface(type);
        this.setUrl(url);
    }

    /**
     * @see com.alibaba.dubbo.rpc.proxy.AbstractProxyInvoker#invoke(Invocation)
     */
    @Override
    public Result invoke(Invocation invocation) throws RpcException {
        try {
            Map<String, String> attachments = RpcContext.getContext().getAttachments();
            if (attachments != null) {
                RpcInvocation rpcInvocation = (RpcInvocation) invocation;
                rpcInvocation.addAttachmentsIfAbsent(attachments);
            }

            return this.httpInvoke(invocation);
        } catch (Throwable e) {
            if (e instanceof RpcException) {
                throw (RpcException) e;
            }

            throw new RpcException(e);
        }
    }

    private Result httpInvoke(Invocation invocation) throws Throwable {
        String serializationKey = this.getUrl().getParameter(Constants.SERIALIZATION_KEY, Consts.SerializationDefault);
        Serialization serialization = SERIALIZATION_EXTENSION_LOADER.getExtension(serializationKey);

        // GenericService
        if (GenericService.class.equals(invocation.getInvoker().getInterface())) {
            invocation = this.genericServiceInvocation(invocation);
        }

        String requestBody = DubboSerializeUtil.serialize(invocation, serialization);

        BudoDubboRpcInvocation budoInvocation = new BudoDubboRpcInvocation(invocation, requestBody);

        // 请求加密
        budoInvocation = this.cipher(budoInvocation);

        if (log.isDebugEnabled()) {
            log.debug("#74 httpInvoke, budoInvocation=" + budoInvocation //
                    + ", serializationKey=" + serializationKey //
                    + ", serialization=" + serialization);
        }

        Map<String, Object> headerMap = new HashMap<String, Object>();

        // 公共 headers
        Map<String, String> commonHeaders = this.commonHeaders(serializationKey, invocation);
        headerMap.putAll(commonHeaders);

        // Sign
        this.addSignHeaders(budoInvocation, headerMap);

        // build request
        Request request = new Request();
        request.setMethod(Request.POST);

        String requestUrl = this.buildRequestUrl();
        request.setUrl(requestUrl);
        request.setHeaders(headerMap);

        String _requestBody = budoInvocation.getRequestBody();
        byte[] _requestBytes = _requestBody.getBytes(Consts.UTF_8);
        request.setRequestBody(_requestBytes);

        log.debug("#149 requestUrl={}, headerMap={}, requestBody={}" + requestUrl, headerMap, _requestBody);

        // 发起请求
        Response response = request.execute();

        // if request error
        if (null == response.getStatus() || response.getStatus().intValue() != 200) {
            String message = "#104 Server returned " + (null == response ? response : response.toSimpleString());
            throw new HttpRequestFailRuntimeException(message);
        }

        // read response
        String headerResultTypeName = response.getHeader(Consts.HeaderKey.ResultType);
        Class<?> headerResultType = BudoReflectionUtil.getResultType(headerResultTypeName);

        // 返回内容解密
        byte[] responseBytes = this.decipher(budoInvocation, response);

        // deserialize
        Object resultEntity = BudoSerializationUtil.deserialize(serialization, responseBytes, headerResultType);

        log.debug("#119 invocation={} , response={} , resultEntity={}", invocation, response, resultEntity);

        return this.processResult(resultEntity, invocation);
    }

    /**
     * 自醒 去服务器端查询 我是否需要sign
     */
    private boolean signCheck(BudoDubboRpcInvocation budoInvocation) {
        try {
            return this.signCheck_0(budoInvocation);
        } catch (Throwable e) {
            log.error("#155 signCheck_0, error e=" + e, e);
            return false;
        }
    }

    private boolean signCheck_0(BudoDubboRpcInvocation budoInvocation) {
        String interfaceName = budoInvocation.interfaceName();

        if (RegistryApi.class.getName().equals(interfaceName)) {
            return false;
        }

        RegistryApi registryApi = this.registryApiReference();

        Boolean signCheck = registryApi.signCheck(interfaceName, budoInvocation.getMethodName(), //
                ReflectUtil.classNameArray(budoInvocation.getParameterTypes()));

        return signCheck;
    }

    private RegistryApi registryApiReference() {
        ReferenceConfig<RegistryApi> referenceConfig = new ReferenceConfig<RegistryApi>();
        referenceConfig.setInterface(RegistryApi.class);
        referenceConfig.setApplication(new ApplicationConfig("155-signCheck"));

        int port = this.getUrl().getPort();
        port = port < 1 ? 80 : port;

        referenceConfig.setUrl("budo-http" + "://" + this.getUrl().getHost() + ":" + port + "/" + this.getUrl().getPath());

        if (null == referenceConfig.getParameters()) {
            referenceConfig.setParameters(new HashMap());
        }

        referenceConfig.getParameters().put(Constants.SERIALIZATION_KEY, Consts.SerializationDefault);

        RegistryApi registryApi = referenceConfig.get();

        log.info("#193 this.url=" + this.getUrl() + ", this=" + this);
        return registryApi;
    }

    /**
     * genericServiceInvocation
     */
    private Invocation genericServiceInvocation(Invocation invocation) {
        String methodName = (String) invocation.getArguments()[0];

        String[] _parameterTypes = (String[]) invocation.getArguments()[1];
        Class<?>[] parameterTypes = ReflectUtil._classForNameArray(_parameterTypes);

        Object[] arguments = (Object[]) invocation.getArguments()[2];

        RpcInvocation _rpcInvocation = new RpcInvocation();
        _rpcInvocation.setMethodName(methodName);
        _rpcInvocation.setParameterTypes(parameterTypes);
        _rpcInvocation.setArguments(arguments);

        _rpcInvocation.setInvoker(invocation.getInvoker());

        return _rpcInvocation;
    }

    /**
     * 返回内容解密
     */
    private byte[] decipher(BudoDubboRpcInvocation budoInvocation, Response response) {
        Cipher cipher = BudoReflectionUtil.getMethodAnnotationCached_1(budoInvocation.interfaceName(), //
                budoInvocation.getMethodName(), //
                budoInvocation.getParameterTypes(), //
                Cipher.class);

        if (null == cipher || null == cipher.value() || cipher.value().isEmpty()) {
            return response.getResponseData(); // 明文返回
        }

        return CipherUtil.decipher(this.cipherService(cipher.value()), budoInvocation, response.getBody());
    }

    /**
     * 传输层加密
     */
    private BudoDubboRpcInvocation cipher(BudoDubboRpcInvocation budoInvocation) {
        Cipher cipher = BudoReflectionUtil.getMethodAnnotationCached_1(budoInvocation.interfaceName(), //
                budoInvocation.getMethodName(), //
                budoInvocation.getParameterTypes(), //
                Cipher.class);

        if (null != cipher && null != cipher.value() && !cipher.value().isEmpty()) {
            return this.cipherService(cipher.value()).cipher(budoInvocation);
        }

        return budoInvocation;
    }

    private void addSignHeaders(BudoDubboRpcInvocation budoInvocation, Map<String, Object> headerMap) {
        String _interface;
        if (GenericService.class.equals(this.getInterface())) {
            _interface = this.getUrl().getParameter("interface");
        } else {
            _interface = this.getInterface().getName();
        }

        budoInvocation.setInterfaceName(_interface);

        // Sign headers
        SignCheck signCheck = BudoReflectionUtil.getMethodAnnotationCached_1(_interface, //
                budoInvocation.getMethodName(), //
                budoInvocation.getParameterTypes(), //
                SignCheck.class);

        boolean needSign = false;
        if (null != signCheck && signCheck.value()) {
            needSign = true;
        } else if (this.signCheck(budoInvocation)) {
            needSign = true;
        } else if ("true".equals(RpcContext.getContext().getAttachment(Consts.SIGN_CHECK))) {
            needSign = true;
        }

        if (!needSign) {
            log.debug("#189 no @SignCheck, signCheck={}, _interface={}, budoInvocation={}, headerMap={}" + signCheck, _interface, budoInvocation, headerMap);

            return;
        }

        SignEntity signHeaders = this.signHeaders(budoInvocation);
        if (null == signHeaders || null == signHeaders.toMap() || signHeaders.toMap().isEmpty()) {
            log.debug("#199 no signHeaders, signHeaders={}, signCheck={}, budoInvocation={}, headerMap={}", signHeaders, signCheck, budoInvocation, headerMap);

            return;
        }

        log.debug("#281 addSignHeaders, headerMap={}, signHeaders={}", headerMap, signHeaders);
        headerMap.putAll(signHeaders.toMap()); // TODO 这些东西是否应该放到 Invocation#getAttachments() 里面去
    }
}
